﻿using System;
using System.Collections.Generic;
using System.Linq;
using kunukn.RecommendationEngine.AreaRecommendation.Code.Entities;
using kunukn.RecommendationEngine.AreaRecommendation.Code.Helpers;

namespace kunukn.RecommendationEngine.AreaRecommendation.Code.Engine
{
    public static class RecommendationAlgorithm
    {
        static class Values
        {
            public const int MaxBestMatchCount = 3;
            public const int MinimumCount = 1;
            public const double MinimumCosineValue = 0.01;
            public const double MinimumJaccardValue = 0.01;
        }

        public const string MaxCount = "Max Count";
        public const string Cosine = "Cosine Similarity";
        public const string JaccardIndex = "Jaccard Index";

        private readonly static Dictionary<string, int> RecommendationIndex;

        static RecommendationAlgorithm()
        {
            RecommendationIndex = new Dictionary<string, int>();
            int i = 0;
            RecommendationIndex.Add(MaxCount, i++);            
            RecommendationIndex.Add(Cosine, i++);            
            RecommendationIndex.Add(JaccardIndex, i++);            
        }

        /// <summary>
        /// Author: Kunuk Nykjaer
        /// Neighborhood function S(x)
        /// apply S(x) on i2i and return most matching
        /// 
        /// This is a simple S(x) function, greedy, use max count
        /// The quality of the recommendation could be improved.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="notSeen"></param>
        /// <returns></returns>
        static List<Item> BestMatchMaxCount(Item x, bool notSeen)
        {
            if (!x.IsValid())
            {
                return new List<Item>();
            }
                
            List<Item> viewed = SessionHelper.GetViewedItems();
            Item2Item i2i = SessionHelper.GetItem2Item();
            List<ItemCount> list = i2i.GetAssociatedSorted(x);

            if (notSeen)
            {
                // the set trick to keep the runtime O(n)
                // filter the seen items from the association
                var set = new HashSet<string>();
                foreach (Item view in viewed)
                {
                    set.Add(view.Id);
                }
                    
                list = list.Where(ic => !set.Contains(ic.Id)).ToList();
            }

            // return recommendation if any association exists with minimum count
            var associatedBestMatch = list.Where(item => item.Count >= Values.MinimumCount).Take(Values.MaxBestMatchCount);
            return associatedBestMatch.Cast<Item>().ToList();
        }


        // O(nlogn) where n is items
        static List<Item> BestMatchCosineSimilarity(Item x, bool notSeen)
        {
            if (!x.IsValid())
            {
                return new List<Item>();
            }                

            Item2Item i2i = SessionHelper.GetItem2Item();
            List<SimilarityResult> list = i2i.GetCosineList(x).OrderByDescending(item => item.Measure).ToList();
            if (notSeen)
            {
                List<Item> viewed = SessionHelper.GetViewedItems();
                // the set trick to keep the runtime O(n)
                // filter the seen items from the association
                var set = new HashSet<string>();
                foreach (Item view in viewed)
                {
                    set.Add(view.Id);
                }
                    
                list = list.Where(sr => !set.Contains(sr.ItemOther.Id)).ToList();
            }

            list = list.Take(Values.MaxBestMatchCount).ToList();
            List<Item> bestMatch = list.Where(item => item.Measure >= Values.MinimumCosineValue).Select(item => item.ItemOther).ToList();
            return bestMatch;
        }

        static List<Item> BestMatchJaccardIndex(Item x, bool notSeen)
        {
            if (!x.IsValid())
            {
                return new List<Item>();
            }                

            Item2Item i2i = SessionHelper.GetItem2Item();
            List<SimilarityResult> list = i2i.GetJaccardIndexList(x).OrderByDescending(item => item.Measure).ToList();
            if (notSeen)
            {
                List<Item> viewed = SessionHelper.GetViewedItems();
                // the set trick to keep the runtime O(n)
                // filter the seen items from the association
                var set = new HashSet<string>();
                foreach (Item view in viewed)
                {
                    set.Add(view.Id);
                }
                    
                list = list.Where(sr => !set.Contains(sr.ItemOther.Id)).ToList();
            }

            list = list.Take(Values.MaxBestMatchCount).ToList();
            List<Item> bestMatch = list.Where(item => item.Measure >= Values.MinimumJaccardValue).Select(item => item.ItemOther).ToList();
            return bestMatch;
        }

        public static List<Item> GetRecommendation(Item currentItem, bool notSeen)
        {
            var bestMatch = new List<Item>();
            if (!currentItem.IsValid())
            {
                return bestMatch;
            }
                
            if (currentItem.IsValid())
            {
                string mode = SessionHelper.GetUserSession().RecommendationMode;
                switch (SessionHelper.GetUserSession().RecommendationMode)
                {
                    case MaxCount:
                        bestMatch = BestMatchMaxCount(currentItem, notSeen);
                        break;                    
                    case Cosine:
                        bestMatch = BestMatchCosineSimilarity(currentItem, notSeen);
                        break;                    
                    case JaccardIndex:
                        bestMatch = BestMatchJaccardIndex(currentItem, notSeen);
                        break;                    
                    default:
                        throw new ApplicationException(string.Format("GetRecommendation() invalid mode: {0}", mode));
                }
            }
            return bestMatch;
        }

        public static int GetRecommendationModeIndex()
        {
            string mode = SessionHelper.GetUserSession().RecommendationMode;
            return RecommendationIndex[mode];
        }

    }
}